use std::{ops, mem::{size_of, self}};
#[allow(dead_code)]
use std::{
    ffi::{CStr, CString},
    os::raw::c_void,
    ptr,
};

use crate::{bindings::*, ffiutils::CBoolean};

/// Raw lua_State wrapper. it doesn't take the ownership of the lua_State pointer.
pub struct RawStateWrapper {
    l: StatePtr,
}

impl RawStateWrapper {
    pub fn new() -> Self {
        unsafe { Self { l: luaL_newstate() } }
    }

    pub fn from_raw(raw: StatePtr) -> Self {
        Self { l: raw }
    }

    pub fn new_userdatauv(&self, size: usize, nuvals: i32) -> *mut c_void {
        unsafe { lua_newuserdatauv(self.l, size, nuvals) }
    }

    pub fn new_userdata<T: Sized>(&self) -> &mut T {
        unsafe {
            self.new_userdatauv(size_of::<T>(), 1)
                .cast::<T>()
                .as_mut()
                .unwrap()
        }
    }

    pub fn open_libs(&self) {
        unsafe { luaL_openlibs(self.l) }
    }

    pub fn push_number(&self, n: f64) {
        unsafe { lua_pushnumber(self.l, n) }
    }

    pub fn push_integer(&self, n: i64) {
        unsafe { lua_pushinteger(self.l, n) }
    }

    pub fn push_lightuserdata<T>(&self, userdata: &mut T) {
        unsafe { lua_pushlightuserdata(self.l, userdata as *mut T as *mut c_void)}
    }

    pub fn push_c_closure(&self, f: CFunction, nupvals: i32) {
        unsafe {
            lua_pushcclosure(self.l, f, nupvals);
        }
    }

    pub fn push_c_function(&self, f: CFunction) {
        self.push_c_closure(f, 0);
    }

    pub fn pcall(&self, nargs: i32, nresults: i32, errfunc: i32) -> ThreadStatus {
        unsafe { lua_pcallk(self.l, nargs, nresults, errfunc, 0, None) }
    }

    pub fn do_string(&self, s: &str) -> ThreadStatus {
        unsafe {
            let s = CString::new(s).unwrap();
            let ans = luaL_loadstring(self.l, s.as_ptr());
            if ans == ThreadStatus::Ok {
                self.pcall(0, -1, 0)
            } else {
                ans
            }
        }
    }

    pub fn get_global(&self, name: &str) -> ValueType {
        let name = CString::new(name).unwrap();
        unsafe { lua_getglobal(self.l, name.as_ptr()) }
    }

    pub fn set_global(&self, name: &str) {
        let name = CString::new(name).unwrap();
        unsafe { lua_setglobal(self.l, name.as_ptr()) }
    }

    pub fn to_numberx(&self, idx: i32, is_num: Option<&mut bool>) -> f64 {
        if let Some(b) = is_num {
            let mut inner_is_num = CBoolean::from(*b);
            let ans = unsafe { lua_tonumberx(self.l, idx, &mut inner_is_num) };
            *b = inner_is_num.into();
            ans
        } else {
            unsafe { lua_tonumberx(self.l, idx, ptr::null_mut()) }
        }
    }

    pub fn to_number(&self, idx: i32) -> f64 {
        self.to_numberx(idx, None)
    }

    pub fn to_integerx(&self, idx: i32, is_num: Option<&mut bool>) -> i64 {
        if let Some(b) = is_num {
            let mut inner_is_num = CBoolean::from(*b);
            let ans = unsafe { lua_tointegerx(self.l, idx, &mut inner_is_num) };
            *b = inner_is_num.into();
            ans
        } else {
            unsafe { lua_tointegerx(self.l, idx, ptr::null_mut()) }
        }
    }

    pub fn to_integer(&self, idx: i32) -> i64 {
        self.to_integerx(idx, None)
    }

    pub fn to_bool(&self, idx: i32) -> bool {
        unsafe { lua_toboolean(self.l, idx) }.into()
    }

    pub fn to_userdata<T>(&self, idx: i32) -> &mut T {
        unsafe { mem::transmute(lua_touserdata(self.l, idx)) }
    }

    pub fn set_top(&self, idx: i32) {
        unsafe { lua_settop(self.l, idx) }
    }

    pub fn pop(&self, n: i32) {
        self.set_top(-n - 1)
    }

    pub fn rotate(&self, idx: i32, n: i32) {
        unsafe { lua_rotate(self.l, idx, n) }
    }

    pub fn remove(&self, idx: i32) {
        self.rotate(idx, -1);
        self.pop(1)
    }

    pub fn typename(&self, t: ValueType) -> &'static str {
        let cs = unsafe { CStr::from_ptr(lua_typename(self.l, t)) };

        cs.to_str().unwrap()
    }

    pub fn value_type(&self, idx: i32) -> ValueType {
        unsafe { lua_type(self.l, idx) }
    }

    pub fn is_function(&self, idx: i32) -> bool {
        self.value_type(idx) == ValueType::Function
    }

    pub fn is_table(&self, idx: i32) -> bool {
        self.value_type(idx) == ValueType::Table
    }

    pub fn is_lightuserdata(&self, idx: i32) -> bool {
        self.value_type(idx) == ValueType::LightUserdata
    }

    pub fn is_nil(&self, idx: i32) -> bool {
        self.value_type(idx) == ValueType::Nil
    }

    pub fn is_boolean(&self, idx: i32) -> bool {
        self.value_type(idx) == ValueType::Boolean
    }

    pub fn is_thread(&self, idx: i32) -> bool {
        self.value_type(idx) == ValueType::Thread
    }

    pub fn is_none(&self, idx: i32) -> bool {
        self.value_type(idx) == ValueType::None
    }

    pub fn is_none_or_nil(&self, idx: i32) -> bool {
        self.value_type(idx) as i32 <= 0
    }

    pub fn close(&self) {
        unsafe { lua_close(self.l) }
    }
}

pub struct State {
    pub raw: RawStateWrapper
}

impl State {
    pub fn new() -> Self {
        Self { raw: RawStateWrapper::new() }
    }
}

impl Drop for State {
    fn drop(&mut self) {
        self.raw.close();
    }
}

impl ops::Deref for State {
    type Target = RawStateWrapper;

    fn deref(&self) -> &Self::Target {
        &self.raw
    }
}

impl ops::DerefMut for State {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.raw
    }
}